C++11 新特性
C++11(原名 C++0x)是自 C++98 以来最大的一次标准更新,几乎所有现代 C++ 写法都从这里开始:统一的并发模型、自动类型推导、lambda、智能指针、变参模板……基本把 C++ 变成了一门「新语言」。本文按「语言特性 / 标准库特性」梳理 C++11 的核心内容,每项都给出用法、原理与常见陷阱,适合作为系统学习或快速查阅的底稿。
编译器支持情况见 Compiler support for C++11。系列总览见 现代 C++ 新特性汇总。
语言特性
内存模型
C++11 定义了标准的内存模型(memory model):一套描述多线程程序中读写操作如何相互可见的规则,是所有并发正确性推理的基础。核心概念:
- happens-before:若操作 A happens-before B,则 A 的效果对 B 可见
- 数据竞争即未定义行为:两个线程并发读写同一非原子变量且至少一个是写,程序直接 UB
- 内存顺序(memory order):默认
memory_order_seq_cst最严格也最慢;acquire/release用于生产者-消费者配对;relaxed只保证单个操作原子性
原理:release 写之前的所有写操作,对 acquire 读之后的读操作可见。没有这层同步,线程 B 可能读到 data == 0。
auto 和 decltype
auto 让编译器从初始化表达式推导变量类型,decltype 从表达式获取其类型而不求值,二者避免类型名的不必要重复:
auto i = 42; // int
auto it = m.begin(); // std::map<...>::iterator,不用写全名
decltype(i) j = 10; // j 的类型与 i 相同:int
decltype(i + 0.5) k = 0; // double
template<typename T, typename U>
auto add(T a, U b) -> decltype(a + b) { // 返回类型依赖参数(尾置返回)
return a + b;
} 原理差异:auto 按模板实参推导规则推导,会丢弃顶层的引用和 const(值语义,产生拷贝);decltype(expr) 严格保留表达式的引用性和 const。这是 C++14 decltype(auto) 存在的原因。
统一初始化
用花括号 {} 对所有类型使用(几乎)完全一致的初始化语法,且禁止收窄转换:
int a{3};
double d{2.5};
int arr[3]{1, 2, 3};
std::vector<int> v{1, 2, 3};
struct Point { int x, y; };
Point p{1, 2};
int x{3.5}; // 错误:double 到 int 是收窄转换
std::vector<int> v1(3, 1); // 圆括号:{1, 1, 1},3 个元素
std::vector<int> v2{3, 1}; // 花括号:{3, 1},2 个元素(优先匹配 initializer_list) 注意最后一组差异:容器如果有 initializer_list 构造函数,花括号会优先选它,这是「统一初始化」最大的陷阱。
nullptr
给空指针一个专有名字,类型为 std::nullptr_t,解决了 NULL(本质是整数 0)的重载歧义:
void f(int);
void f(const char*);
f(NULL); // 调用 f(int)!NULL 展开为 0,与直觉相反
f(nullptr); // 明确调用 f(const char*) 原理:nullptr 不是整数类型,只能转换成指针类型,因此重载决议永远选指针版本。
constexpr 函数
constexpr 声明的函数可以在编译期求值,把一部分运行期计算搬到编译期:
constexpr int factorial(int n) {
return n <= 1 ? 1 : n * factorial(n - 1); // C++11 限制:只能有这一条 return
}
int buf[factorial(5)]; // 编译期定长数组,120 个元素
static_assert(factorial(4) == 24, ""); 原理:编译器在常量求值上下文(数组大小、模板实参、static_assert 等)中用解释执行的方式求值函数;同样的函数运行期也照常调用,不是宏替换。C++11 语法限制很严(单 return),C++14 起放松。
用户定义字面量
为自定义类型提供字面量后缀,运算符写法是 operator"" 后缀名:
constexpr long double operator""_km(long double v) { return v * 1000.0L; }
constexpr long double operator""_m(long double v) { return v; }
auto meters = 1.5_km + 30_m; // 1530 米,单位进了类型系统 原理:1.5_km 本质是对 operator""_km(1.5L) 的调用,编译器把字面量翻译成函数调用,单位检查、量纲运算都可以在编译期完成。标准库自己在 C++14 起也用了这套机制(""s、""h)。
lambda 表达式
lambda 是匿名函数对象(闭包),配合算法省去定义命名仿函数:
std::vector<int> v{3, 1, 4, 1, 5};
std::sort(v.begin(), v.end(), [](int a, int b) { return a > b; });
int threshold = 3;
auto over = std::count_if(v.begin(), v.end(),
[threshold](int x) { return x > threshold; }); // 按值捕获
auto bump = [&threshold](int x) { threshold += x; }; // 按引用捕获 原理:编译器为每个 lambda 生成一个匿名的仿函数类型,捕获的变量成为其成员变量——[threshold](int x){ return x > threshold; } 大致等价于:
struct __Closure {
int threshold; // 捕获即成员
bool operator()(int x) const { return x > threshold; }
}; 所以 lambda 与手写仿函数性能完全一致,且可以用 std::function 保存。按值捕获默认不可修改,加 mutable 才能改副本。
变参模板
变参模板(variadic template)用参数包接受任意个任意类型的参数,是完美转发、make_shared、emplace_back、tuple 的底层机制:
void print() {} // 递归终止版本
template<typename T, typename... Rest>
void print(const T& first, const Rest&... rest) {
std::cout << first << ' ';
print(rest...); // 递归展开,空包时匹配终止版本
}
print(1, "two", 3.0); // 任意个数、任意类型
// sizeof...(rest) 可取包中元素个数 原理:编译器对参数包做递归实例化,每次剥掉最前面一个参数,直到包为空。C++17 的折叠表达式让这类代码不再需要递归。
模板别名
using 可以为模板起别名并绑定部分模板参数,这是 typedef 做不到的:
template<typename T>
using Vec = std::vector<T, MyAlloc<T>>; // 别名模板:保留了一个模板参数
Vec<int> v; // 等价 std::vector<int, MyAlloc<int>>
typedef std::vector<int, MyAlloc<int>> IntVec; // typedef 只能绑定成完整类型 标准库大量使用它:std::string = std::basic_string<char>,以及 C++14 的 remove_reference_t 一族元函数别名。
noexcept
noexcept 明确承诺函数不抛异常(违反则 std::terminate),还可以带条件:
void swap_all(int& a, int& b) noexcept; // 无条件承诺
template<typename T>
void my_swap(T& a, T& b)
noexcept(noexcept(T(std::move(a)))) // 条件 noexcept:移动构造不抛才承诺
{
T tmp = std::move(a);
a = std::move(b);
b = std::move(tmp);
} 它不只是文档:std::vector 扩容时若元素移动构造是 noexcept 的,就用移动否则退回拷贝,以维持强异常安全。所以自定义类型的移动构造/交换应尽量标 noexcept,性能差别可达一个数量级。
override 和 final
显式标注虚函数覆盖关系,把「签名写错导致新开虚函数」这类错误从运行期行为异常变成编译错误:
struct Base {
virtual void draw() const;
virtual void resize(int) final; // 不允许再被覆盖
};
struct Derived : Base {
void draw() const override; // 明确:覆盖 Base::draw
// void draw() override; // 少了 const -> 编译错误(不是覆盖)
};
struct Node : Base {
void resize(int) override; // 编译错误:Base::resize 已是 final
}; final 也可用于整个类,禁止进一步继承,给编译器去虚化的机会。
static_assert
编译期断言,条件不满足直接编译失败:
static_assert(sizeof(void*) >= 8, "require 64-bit platform");
static_assert(sizeof(Header) == 32, "wire format requires exact size"); 与运行期 assert 互补:static_assert 检查的是编译期已知的事实,零运行期开销。
long long
至少 64 位的有符号整数类型(long long / unsigned long long),后缀 LL / ULL:
long long big = 9007199254740992LL; // 超出 32 位范围的整数运算不再依赖平台扩展 背景:C++03 里 long 在 Windows 上只有 32 位,跨平台 64 位整数只能靠扩展(int64_t 进标准库也是 C++11)。
默认成员初始化器
NSDMI(non-static data member initializer)给数据成员写默认值,构造函数不覆盖时生效:
struct Widget {
int id = 0;
std::string name = "anon";
std::vector<int> buf{16}; // 也可以用花括号
};
Widget w; // id == 0, name == "anon", buf 有 16 个 0
Widget w2{7}; // id == 7,其余仍用默认值 好处:多个构造函数不再需要重复写相同的成员初始化列表。
enum class
作用域枚举(强类型枚举):枚举值有作用域、不隐式转成整数、可指定底层类型:
enum class Color : std::uint8_t { red, green, blue };
Color c = Color::red; // 必须带作用域
// int n = c; // 错误:不隐式转整数
auto n = static_cast<int>(c); // 显式转换才允许
// Color::red == TrafficLight::red // 不同枚举互不冲突,老式 enum 的问题消失 解决老 enum 的三个毛病:名字泄漏到外层作用域、隐式转整数、无法指定底层类型。
标准库特性
unique_ptr 和 shared_ptr
依赖 RAII 的资源管理指针,把 new/delete 的生命周期交给对象作用域:
// unique_ptr:独占所有权,零开销抽象(大小 = 裸指针,析构即 delete)
std::unique_ptr<FILE, decltype(&fclose)> f(fopen("a.txt", "r"), &fclose);
auto up = std::unique_ptr<int>{new int{42}}; // make_unique 要到 C++14
// shared_ptr:共享所有权,最后一个引用析构时释放对象
auto sp = std::make_shared<Widget>(args); // 推荐:一次分配同时放对象和控制块
// weak_ptr:打破循环引用(父子互持时把一方改成 weak)
std::weak_ptr<Node> parent;
if (auto p = parent.lock()) { /* 对象还活着 */ } 原则:默认用 unique_ptr,确实需要共享才上 shared_ptr;引用计数是原子操作,有跨线程使用才有真实成本。
内存模型和 atomic 变量
std::atomic<T> 提供免锁的原子读写和 RMW(读-改-写)操作,是内存模型在库层的落点:
std::atomic<int> counter{0};
counter.fetch_add(1, std::memory_order_relaxed); // 计数器:只要原子性,不约束顺序
// CAS:无锁数据结构的基本原语
int expected = 0;
bool ok = counter.compare_exchange_weak(expected, 1); // 相等则写入 1
// 自旋锁:acquire/release 的最小示例
std::atomic_flag lock = ATOMIC_FLAG_INIT;
while (lock.test_and_set(std::memory_order_acquire)) {} // 上锁
// ... 临界区 ...
lock.clear(std::memory_order_release); // 解锁 原则:能用互斥锁就用互斥锁,atomic 只用于低竞争的简单状态位和计数器;std::atomic<T>::is_lock_free() 可查询是否真的免锁。
thread、mutex 和 condition_variable
系统层级并发的类型安全、可移植封装:
std::queue<int> q;
std::mutex m;
std::condition_variable cv;
void producer() {
{
std::lock_guard<std::mutex> lk(m);
q.push(42);
} // 离开作用域自动解锁
cv.notify_one();
}
void consumer() {
std::unique_lock<std::mutex> lk(m);
cv.wait(lk, [] { return !q.empty(); }); // 谓词循环防虚假唤醒
int x = q.front();
q.pop();
} 要点:锁的 RAII 包装(lock_guard/unique_lock)杜绝忘解锁;condition_variable::wait 必须配合谓词使用,否则虚假唤醒会穿透。
future、promise 和 packaged_task
比线程+互斥锁稍高级的「一次性结果传递」机制:
// promise/future:手工建立传递通道
std::promise<int> pr;
std::future<int> fu = pr.get_future();
std::thread t([&] { pr.set_value(42); }); // 也可 set_exception 传异常
int v = fu.get();
t.join();
// packaged_task:把可调用对象包装成可设置到 future 的任务
std::packaged_task<int(int)> task([](int x) { return x * x; });
auto fu2 = task.get_future();
std::thread(std::move(task), 6).detach();
// fu2.get() == 36
// async:最高层,直接拿 future
auto fu3 = std::async(std::launch::async, [] { return 7; }); 注意:std::async 返回的 future 析构时会阻塞等待任务完成,不适合「发后不管」的用法。
tuple
匿名的简单复合类型,把异构值打包成一个对象:
auto t = std::make_tuple(1, 2.5, "three");
int i = std::get<0>(t);
// 一次解包多个返回值
double d; const char* s;
std::tie(std::ignore, d, s) = t; // std::ignore 占位跳过
std::tuple<std::string, int> split("age", 42);
auto& [key, val] = split; // C++17 结构化绑定是它的现代用法 类型特征
type traits 是类型的可测试属性,元编程的基础构件,全部在 <type_traits>:
static_assert(std::is_integral<int>::value, ""); // ::value 是 C++11 写法
static_assert(std::is_pointer<int*>::value, "");
// 典型用法:enable_if 按条件选择重载(SFINAE)
template<typename T>
typename std::enable_if<std::is_integral<T>::value, T>::type
half(T x) { return x / 2; } // 只对整数类型存在
// 常用变换:去掉引用/const
static_assert(std::is_same<std::remove_reference<int&>::type, int>::value, ""); 原理:trait 本体是个携带 value 或类型别名的模板,靠特化给出每个类型的具体答案;编译期「查询」代替运行期类型信息。
正则表达式
<regex> 提供 ECMAScript 语法的正则匹配:
#include <regex>
std::string text = "mail: tom@corp.com, jerry@home.org";
std::regex re(R"((\w+)@([\w.]+))");
auto begin = std::sregex_iterator(text.begin(), text.end(), re);
for (auto it = begin; it != std::sregex_iterator{}; ++it) {
std::cout << "user=" << (*it)[1] << " domain=" << (*it)[2] << '\n';
} 注意:std::regex 构造开销大且线程不安全(构造后只读使用是安全的),应做成 static const;性能敏感场景用 RE2 等。
随机数
新的随机数框架 = 引擎(生成原始位序列)+ 分布(映射到需要的分布),替代质量差且有偏的 rand():
// rand() % 6 有偏(区间不整除),且全局状态、质量差
std::mt19937 rng{std::random_device{}()}; // 梅森旋转引擎,随机种子
std::uniform_int_distribution<int> die(1, 6); // 均匀整数分布
std::normal_distribution<double> noise(0.0, 1.0); // 正态分布
int roll = die(rng); 原则:引擎和分布分离,同一个引擎配不同分布;引擎是可拷贝的有状态对象,可保存/恢复实验现场。
时间
<chrono> 用类型安全的 duration(时长)和 time_point(时刻)替代整数毫秒:
using namespace std::chrono;
auto t0 = steady_clock::now(); // 单调时钟,测耗时用它
do_work();
auto t1 = steady_clock::now();
auto us = duration_cast<microseconds>(t1 - t0).count(); // 显式单位转换
auto relaxed = 1.5s; // 字面量是 C++14,但类型 C++11 已有
milliseconds d(1500); 关键点:不同单位(nanoseconds/milliseconds...)是不同类型,隐式转换只允许放大不允缩小,duration_cast 显式承担有损转换;墙上时钟用 system_clock,单调计时用 steady_clock。
unordered_map 等哈希表
unordered_map / unordered_set / unordered_multimap / unordered_multiset 是哈希表实现:
std::unordered_map<std::string, int> um;
um["cpp"] = 11; // 平均 O(1)
um.insert({"stl", 98});
auto it = um.find("cpp"); // find 平均 O(1)
if (it != um.end())
std::cout << it->second; | 对比项 | map(红黑树) | unordered_map(哈希表) |
|---|---|---|
| 查找 | O(log n) | 平均 O(1),最坏 O(n) |
| 迭代顺序 | 按 key 有序 | 无序 |
| key 要求 | 可比较 | 可哈希 |
| 适用 | 需要有序遍历/范围查询 | 纯点查 |
自定义 key 类型需要提供 std::hash 特化和 operator==。
array
固定长度的数组容器,「会记住自己大小、不会退化成指针的原生数组」:
std::array<int, 3> a{1, 2, 3};
std::cout << a.size(); // 3:大小是类型的一部分
std::sort(a.begin(), a.end());
int* p = a.data(); // 需要裸指针时显式取
void f(int arr[]); // 原生数组传参退化成指针,长度丢失
void g(std::array<int, 3> arr); // array 不退化 array 在栈上分配(无堆分配),是聚合类型,性能与原生数组一致。
emplace 运算
emplace 系列在容器内直接构造对象,参数完美转发给构造函数,避免临时对象和拷贝:
std::vector<std::pair<std::string, int>> v;
v.push_back(std::make_pair("a", 1)); // 构造临时 pair 再拷贝/移动进去
v.emplace_back("b", 2); // 原地调用 pair(string, int) 构造
std::map<std::string, Widget> m;
m.emplace("key", 42); // map 也有 emplace/emplace_hint 原理:emplace_back(Args&&... args) 在容器内存位置上用 T(std::forward<Args>(args)...) 构造,省掉「先造临时再搬进去」的整个环节。
exception_ptr
允许把异常对象打包,跨线程或延迟重新抛出:
std::exception_ptr eptr;
try {
risky();
} catch (...) {
eptr = std::current_exception(); // 捕获当前异常但不立刻处理
}
// 在别的线程 / 稍后
if (eptr) {
try { std::rethrow_exception(eptr); } // 重新抛出原异常
catch (const std::exception& e) { log(e.what()); }
} 它就是线程池把工作线程的异常传回提交线程的标准通道(promise::set_exception 内部也用它)。
常见清单未列、但同版本核心
一些清单(如《A Tour of C++》附录)没有列出、但 C++11 同样核心的特性:
右值引用与移动语义:
T&&引用可被修改的临时对象,std::move把左值标记为可移动,移动构造直接窃取资源(如 string 的缓冲区)而不是深拷贝。它是unique_ptr、emplace、noexcept优化等一大串特性的地基:cppstd::string a = "a long enough string to avoid SSO"; std::string b = std::move(a); // O(1):窃取缓冲区指针,a 进入有效但未指定状态范围 for:
for (const auto& x : v) ...,任何提供begin()/end()的类型都能用thread_local:线程局部存储,每个线程一份独立副本的静态变量
std::function:类型擦除的可调用对象容器,统一存 lambda、函数指针、仿函数
小结
C++11 的主线是三件事:值语义的资源管理(移动语义 + 智能指针)、编译期计算(constexpr + 模板体系增强)、并发进入语言(内存模型 + 线程库)。后续版本大多是在这三条线上继续加深——C++14 放松限制、C++17 补词汇类型、C++20 引入概念与协程。理解了 C++11,后面的版本演进都有了参照系。
系列其他篇目:现代 C++ 新特性汇总 · C++14 新特性 · C++17 新特性 · C++20 新特性 · C++23 新特性